package it.unicam.cs.cosy.bioshape.core.shapes;

import javax.vecmath.Point3d;
import javax.vecmath.Tuple3d;

/**
 * This class defines an axis aligned bounding box which is used for bounding
 * regions. The AABB is chosen big enough to contain also the <i>transformed</i>
 * shape. Indeed the bounding vertex are set at the distance equal to the radius
 * of the sphere containing the entity. If the minimal vertex is higher than the
 * maximal one the box is set {@code empty}, i.e. it cannot intersect with any
 * other bound.
 */
public class BoundingBox extends Bounds {

	/** Generated serialization ID */
	private static final long serialVersionUID = -2819829207022895005L;

	Point3d center, min, max;

	/**
	 * Builds the bounding volume given the minimal and maximal vertexes in
	 * x,y,z. The box can be <i>any</i> parallelepiped with only right angles.
	 * 
	 * @param min
	 *            the smallest corner
	 * @param max
	 *            the largest corner
	 */
	public BoundingBox(Point3d min, Point3d max) {
		if (min.x > max.x || min.y > max.y || min.z > max.z)
			throw new IllegalArgumentException("Bounding box minimal vertex "
					+ "cannot be greater than maximal vertex...");
		boundId = BOUNDING_BOX;
		this.min = new Point3d(min);
		this.max = new Point3d(max);
		center = new Point3d((max.x + min.x) / 2, (max.y + min.y) / 2,
				(max.z + min.z) / 2);
	}

	/**
	 * Builds a bounding box axis-aligned (AABB) starting from the centre and
	 * the radius of the entity.
	 * 
	 * @param center
	 *            the centre of the entity
	 * @param radius
	 *            the radius of the enclosing sphere of the entity
	 */
	public BoundingBox(Tuple3d center, double radius) {
		if (radius < 0)
			throw new IllegalArgumentException("Bounding box radius cannot be "
					+ "negative...");
		this.center = new Point3d(center);
		boundId = BOUNDING_BOX;
		//
		min = new Point3d();
		max = new Point3d();
		//
		min.x = center.x - radius;
		min.y = center.y - radius;
		min.z = center.z - radius;
		//
		max.x = center.x + radius;
		max.y = center.y + radius;
		max.z = center.z + radius;
	}

	@Override
	protected Object clone() {
		return new BoundingBox(min, max);
	}

	@Override
	public boolean contains(Bounds boundsObject) {
		if (boundsObject == null)
			return false;
		//
		if (boundsObject.boundId == BOUNDING_BOX) {
			BoundingBox box = (BoundingBox) boundsObject;
			// both boxes are axis aligned
			if (box.min.x < min.x || box.max.x > max.x || box.min.y < min.y
					|| box.max.y > max.y || box.min.z < min.z
					|| box.max.z > max.z)
				return false;
			return true;
		} else if (boundsObject.boundId == BOUNDING_SPHERE) {
			BoundingSphere s = (BoundingSphere) boundsObject;
			if (s.center.x - s.radius < min.x || s.center.x + s.radius > max.x
					|| s.center.y - s.radius < min.y
					|| s.center.y + s.radius > max.y
					|| s.center.z - s.radius < min.z
					|| s.center.z + s.radius > max.z)
				// double semiDiagonal = (Math.sqrt(3) * sphere.radius * 2) / 2;
				// Point3d one = new Point3d(sphere.center);
				// Point3d two = new Point3d(sphere.center);
				// one.scale(-semiDiagonal);
				// two.scale(semiDiagonal);
				// BoundingBox sBox = new BoundingBox(one, two);
				// if (sBox.min.x < min.x || sBox.max.x > max.x || sBox.min.y <
				// min.y
				// || sBox.max.y > max.y || sBox.min.z < min.z
				// || sBox.max.z > max.z)
				return false;
			return true;
		} else
			throw new IllegalArgumentException("Wrong bounding box!");
	}

	@Override
	public boolean equals(Object bounds) {
		if (this == bounds)
			return true;
		try {
			BoundingBox box = (BoundingBox) bounds;
			return (min.equals(box.min) && max.equals(box.max));
		} catch (NullPointerException e) {
			return false;
		} catch (ClassCastException e) {
			return false;
		}
	}

	@Override
	public Point3d getCenter() {
		return center;
	}

	/**
	 * Getter.
	 * 
	 * @return the maximal point of this bounding box
	 */
	public Point3d getMax() {
		return max;
	}

	@Override
	public double getMax(int index) {
		if (index == 0)
			return max.getX();
		if (index == 1)
			return max.getY();
		return max.getZ();
	}

	/**
	 * Getter.
	 * 
	 * @return the minimal point of this bounding box
	 */
	public Point3d getMin() {
		return min;
	}

	@Override
	public double getMin(int index) {
		if (index == 0)
			return min.getX();
		if (index == 1)
			return min.getY();
		return min.getZ();
	}

	@Override
	public double getRadius() {
		return center.distance(min);
	}

	@Override
	public int hashCode() {
		long bits = 1L;
		bits = 31L * bits + Double.doubleToLongBits(min.x);
		bits = 31L * bits + Double.doubleToLongBits(min.y);
		bits = 31L * bits + Double.doubleToLongBits(min.z);
		bits = 31L * bits + Double.doubleToLongBits(max.x);
		bits = 31L * bits + Double.doubleToLongBits(max.y);
		bits = 31L * bits + Double.doubleToLongBits(max.z);
		return (int) (bits ^ (bits >> 32));
	}

	@Override
	public boolean intersect(Bounds boundsObject) {
		if (boundsObject == null)
			return false;
		//
		if (boundsObject.boundId == BOUNDING_BOX) {
			BoundingBox box = (BoundingBox) boundsObject;
			// both boxes are axis aligned
			if (max.x > box.min.x && box.max.x > min.x && max.y > box.min.y
					&& box.max.y > min.y && max.z > box.min.z
					&& box.max.z > min.z)
				return true;
			return false;
		} else if (boundsObject.boundId == BOUNDING_SPHERE) {
			BoundingSphere sphere = (BoundingSphere) boundsObject;
			double rad_sq = sphere.radius * sphere.radius;
			//
			double dis = 0.0;
			double app;
			if ((app = sphere.center.x - min.x) < 0) {
				if (app < -sphere.radius)
					return false;
				dis = app * app;
			} else if ((app = sphere.center.x - max.x) > 0) {
				if (app > sphere.radius)
					return false;
				dis = app * app;
			}
			if ((app = sphere.center.y - min.y) < 0) {
				if (app < -sphere.radius)
					return false;
				dis += app * app;
			} else if ((app = sphere.center.y - max.y) > 0) {
				if (app > sphere.radius)
					return false;
				dis += app * app;
			}
			if ((app = sphere.center.z - min.z) < 0) {
				if (app < -sphere.radius)
					return false;
				dis += app * app;
			} else if ((app = sphere.center.z - max.z) > 0) {
				if (app > sphere.radius)
					return false;
				dis += app * app;
			}
			//
			return (dis <= rad_sq);
		} else
			throw new IllegalArgumentException("Wrong bounding box!");
	}

	@Override
	public boolean intersect(Tuple3d point) {
		if (point.x <= max.x && point.x >= min.x && point.y <= max.y
				&& point.y >= min.y && point.z <= max.z && point.z >= min.z)
			return true;
		return false;
	}

	@Override
	public void moveTo(double x, double y, double z) {
		double offX = x - center.x;
		double offY = y - center.y;
		double offZ = z - center.z;
		center.set(x, y, z);
		//
		min.x += offX;
		min.y += offY;
		min.z += offZ;
		//
		max.x += offX;
		max.y += offY;
		max.z += offZ;
	}

	@Override
	public void moveTo(Tuple3d point) {
		moveTo(point.x, point.y, point.z);
	}

	@Override
	public void set(Bounds boundsObject) {
		if (boundsObject.boundId == BOUNDING_BOX) {
			BoundingBox box = (BoundingBox) boundsObject;
			min.x = box.min.x;
			min.y = box.min.y;
			min.z = box.min.z;
			max.x = box.max.x;
			max.y = box.max.y;
			max.z = box.max.z;
			center = box.center;
		} else if (boundsObject.boundId == BOUNDING_SPHERE) {
			BoundingSphere sphere = (BoundingSphere) boundsObject;
			min.x = sphere.center.x - sphere.radius;
			min.y = sphere.center.y - sphere.radius;
			min.z = sphere.center.z - sphere.radius;
			max.x = sphere.center.x + sphere.radius;
			max.y = sphere.center.y + sphere.radius;
			max.z = sphere.center.z + sphere.radius;
			center = sphere.center;
		} else
			throw new IllegalArgumentException("Bouding box not valid!");
	}

	@Override
	public void translateOf(Tuple3d offset) {
		translateOf(offset.x, offset.y, offset.z);
	}

	@Override
	public void translateOf(double offX, double offY, double offZ) {
		center.x += offX;
		center.y += offY;
		center.z += offZ;
		//
		min.x += offX;
		min.y += offY;
		min.z += offZ;
		//
		max.x += offX;
		max.y += offY;
		max.z += offZ;
	}
}